/**
 * Shelf2D.java Created on : 13 juin 07
 */
package fr.limsi.seven;

import java.awt.Color;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Ellipse2D;
import java.awt.geom.GeneralPath;
import java.util.LinkedList;

import javax.swing.BorderFactory;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.Timer;

/**
 * @author Camille
 */
public class Shelf2D extends JPanel {

	/**
	 * Auto-generated serial version UID number
	 */
	private static final long serialVersionUID = -549492409116408812L;

	/**
	 * The resize tolerance constant (i.e. the maximum mouse distance to an edge in order to accept entering in resize mode)
	 */
	public static final int RESIZE_EPSILON = 5;

	/**
	 * Minimal shelf size
	 */
	public static final int LIMIT_SIZE = 30;

	/**
	 * Shelf buttons size
	 */
	public final int BUTTON_SIZE = 10;

	/**
	 * Default shelf height
	 */
	public static final int DEFAULT_HEIGHT = 30;

	/**
	 * Margins for left and right edges
	 */
	public static final int MARGINS_LR = 20;

	/**
	 * Matgins for top and bottom edges
	 */
	public static final int MARGINS_TB = 10;

	/**
	 * Shelf node width
	 */
	public static final int NODE_WIDTH = 7;

	/**
	 * Trapezoid top edge width delta (in pixel) ratio to the bottom edge
	 */
	public static final int TRAPEZOID_DELTA = 15;

	/**
	 * Shelf opacity
	 */
	public static final float OPACITY = 0.55f;

	/**
	 * Animation callback delay in milliseconds
	 */
	public static final int ANIMATION_DELAY = 10;

	/**
	 * Animation length in images count
	 */
	public static final int ANIMATION_LENGTH = 100;

	/**
	 * Timer for currentAnimation operations
	 */
	protected Timer animationTimer;

	/**
	 * Mouse X coordinate
	 */
	protected int mouseX;

	/**
	 * Mouse Y coordinate
	 */
	protected int mouseY;

	/**
	 * Resize mode indicator
	 */
	protected boolean resizingTop;

	/**
	 * Resize mode indicator
	 */
	protected boolean resizingBottom;

	/**
	 * Resize mode indicator
	 */
	protected boolean resizingLeft;

	/**
	 * Resize mode indicator
	 */
	protected boolean resizingRight;

	/**
	 * The shelf selection zone {@link GeneralPath}
	 */
	protected GeneralPath selectionZone;

	/**
	 * First color for the gradient paint
	 */
	protected Color gradientColorBegin;

	/**
	 * Second color for the gradient paint
	 */
	protected Color gradientColorEnd;

	/**
	 * Close button
	 */
	protected JLabel closeButton;

	/**
	 * Associated {@link Pyramide2D}
	 */
	protected Pyramide2D pyramide2D;

	/**
	 * List of nodes that are on the shelf (organized by level)
	 */
	protected LinkedList<LinkedList<MiniNode2D>> nodesOnShelf;

	/**
	 * Default constructor
	 * 
	 * @param view
	 *            the associated view
	 */
	public Shelf2D(Pyramide2D view) {

		this.pyramide2D = view;

		nodesOnShelf = new LinkedList<LinkedList<MiniNode2D>>();

		gradientColorBegin = new Color(0, 0.6f, 1.0f, OPACITY);
		gradientColorEnd = new Color(0, 0.8f, 1.0f, OPACITY);

		closeButton = new JLabel(new ImageIcon(getClass().getResource("/fr/limsi/seven/images/close.png")));
		closeButton.setBounds(0, 0, BUTTON_SIZE, BUTTON_SIZE);
		add(closeButton);

		setOpaque(false);
		setBorder(BorderFactory.createLineBorder(new Color(0, 0, 0, 0), 2));

		buildInteractions();
	}

	/**
	 * Creates and adds all the shelf mouse interactions
	 */
	protected void buildInteractions() {

		/* ----------- Shelf interactions ----------- */

		MouseAdapter mA = new MouseAdapter() {

			@Override
			public void mouseEntered(MouseEvent e) {
				setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
				setBorder(BorderFactory.createLineBorder(Color.ORANGE, 2));
			}

			@Override
			public void mouseExited(MouseEvent e) {
				setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
				setBorder(BorderFactory.createLineBorder(new Color(0, 0, 0, 0), 2));
			}

			@Override
			public void mousePressed(MouseEvent e) {
				mouseX = e.getXOnScreen();
				mouseY = e.getYOnScreen();
			}

			@Override
			public void mouseReleased(MouseEvent e) {
				clearNodesOnShelf();
				nodesOnShelf = getNodesInSelectionZone();
				alignNodesOnShelf();
				repaint();
			}

			@Override
			public void mouseClicked(MouseEvent e) {
				if (e.getClickCount() == 2) {
					pyramide2D.getAnimationMonitor().queueAnimation(getClosingAnimation());
					pyramide2D.getAnimationMonitor().queueAnimation(pyramide2D.getAnimationBuilder().getColorAnimation(pyramide2D.getDefaultDrawingColor()));
				}
			}

			@Override
			public void mouseMoved(MouseEvent e) {

				resizingTop = false;
				resizingBottom = false;
				resizingLeft = false;
				resizingRight = false;

				setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));

				// TOP resize
				if (e.getY() >= -RESIZE_EPSILON && e.getY() <= RESIZE_EPSILON) {
					resizingTop = true;
					setCursor(Cursor.getPredefinedCursor(Cursor.N_RESIZE_CURSOR));
				}

				// BOTTOM resize
				if (e.getY() >= getHeight() - RESIZE_EPSILON && e.getY() <= getHeight() + RESIZE_EPSILON) {
					resizingBottom = true;
					setCursor(Cursor.getPredefinedCursor(Cursor.S_RESIZE_CURSOR));
				}

				// LEFT resize
				if (e.getX() >= -RESIZE_EPSILON && e.getX() <= RESIZE_EPSILON) {
					resizingLeft = true;

					if (resizingTop) {
						setCursor(Cursor.getPredefinedCursor(Cursor.NW_RESIZE_CURSOR));
					}
					else {
						if (resizingBottom) {
							setCursor(Cursor.getPredefinedCursor(Cursor.SW_RESIZE_CURSOR));
						}
						else {
							setCursor(Cursor.getPredefinedCursor(Cursor.W_RESIZE_CURSOR));
						}
					}
				}

				// RIGHT resize
				if (e.getX() >= getWidth() - RESIZE_EPSILON && e.getX() <= getWidth() + RESIZE_EPSILON) {
					resizingRight = true;

					if (resizingTop) {
						setCursor(Cursor.getPredefinedCursor(Cursor.NE_RESIZE_CURSOR));
					}
					else {
						if (resizingBottom) {
							setCursor(Cursor.getPredefinedCursor(Cursor.SE_RESIZE_CURSOR));
						}
						else {
							setCursor(Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR));
						}
					}
				}
			}

			@Override
			public void mouseDragged(MouseEvent e) {

				if (!resizingTop && !resizingBottom && !resizingLeft && !resizingRight) {
					setLocation(getLocation().x + (e.getXOnScreen() - mouseX), getLocation().y + (e.getYOnScreen() - mouseY));
				}
				else {
					if (resizingTop && (getHeight() - (e.getYOnScreen() - mouseY)) > LIMIT_SIZE) {
						setLocation(getX(), getY() + (e.getYOnScreen() - mouseY));
						setSize(new Dimension(getWidth(), getHeight() - (e.getYOnScreen() - mouseY)));
					}

					if (resizingBottom && (getHeight() + (e.getYOnScreen() - mouseY)) > LIMIT_SIZE) {
						setSize(new Dimension(getWidth(), getHeight() + (e.getYOnScreen() - mouseY)));
					}

					if (resizingLeft && (getWidth() - (e.getXOnScreen() - mouseX)) > LIMIT_SIZE) {
						setLocation(getX() + (e.getXOnScreen() - mouseX), getY());
						setSize(new Dimension(getWidth() - (e.getXOnScreen() - mouseX), getHeight()));
					}

					if (resizingRight && (getWidth() + (e.getXOnScreen() - mouseX)) > LIMIT_SIZE) {
						setSize(new Dimension(getWidth() + (e.getXOnScreen() - mouseX), getHeight()));
					}
				}

				mouseX = e.getXOnScreen();
				mouseY = e.getYOnScreen();

				repaint();
				pyramide2D.repaint();
			}
		};

		addMouseListener(mA);
		addMouseMotionListener(mA);

		/* ----------- Buttons interactions ----------- */

		MouseAdapter closeListener = new MouseAdapter() {

			@Override
			public void mouseEntered(MouseEvent e) {
				setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
			}

			@Override
			public void mouseExited(MouseEvent e) {
				setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
			}

			@Override
			public void mouseClicked(MouseEvent e) {
				pyramide2D.getAnimationMonitor().queueAnimation(getClosingAnimation());
			}
		};

		closeButton.addMouseListener(closeListener);
	}

	/**
	 * Empties the <i>nodesOnShelf</i> list
	 */
	protected void clearNodesOnShelf() {
		while (!nodesOnShelf.isEmpty()) {
			LinkedList<MiniNode2D> level = nodesOnShelf.getFirst();
			nodesOnShelf.remove(level);
		}
	}

	/**
	 * Getter for all the {@link Node2D} inside the <i>selectionZone</i>
	 * 
	 * @return the list of {@link MiniNode2D} corresponding to the {@link Node2D} which are inside the <i>selectionZone</i> organized by level (i.e. a <i>LinkedList< LinkedList<MiniNode2D> ></i>)
	 */
	public LinkedList<LinkedList<MiniNode2D>> getNodesInSelectionZone() {

		LinkedList<LinkedList<MiniNode2D>> nodesIn = new LinkedList<LinkedList<MiniNode2D>>();
		LinkedList<MiniNode2D> levelMatches;

		/* Base */
		levelMatches = new LinkedList<MiniNode2D>();

		for (Node2D node2D : pyramide2D.getBase()) {
			if (selectionZone.contains(node2D.getLocationOnScreen().x - getLocationOnScreen().x, node2D.getLocationOnScreen().y - getLocationOnScreen().y)) {
				levelMatches.add(new MiniNode2D(node2D));
			}
		}

		if (!levelMatches.isEmpty()) {
			nodesIn.add(levelMatches);
		}

		/* Other levels */
		for (LinkedList<Node2D> level : pyramide2D.getAllLevels()) {

			levelMatches = new LinkedList<MiniNode2D>();

			for (Node2D node2D : level) {
				if (selectionZone.contains(node2D.getLocationOnScreen().x - getLocationOnScreen().x, node2D.getLocationOnScreen().y - getLocationOnScreen().y)) {
					levelMatches.add(new MiniNode2D(node2D));
				}
			}

			if (!levelMatches.isEmpty()) {
				nodesIn.add(levelMatches);
			}
		}

		return nodesIn;
	}

	/**
	 * Aligns the node that are currently on the shelf
	 */
	protected void alignNodesOnShelf() {

		if (nodesOnShelf.size() > 0) {

			int y;
			int vSpace;

			if (nodesOnShelf.size() > 1) {
				y = getHeight() - MARGINS_TB - NODE_WIDTH;
				vSpace = (getHeight() - (nodesOnShelf.size() * NODE_WIDTH) - (2 * MARGINS_TB)) / (nodesOnShelf.size() - 1);
			}
			else {
				y = getHeight() / 2;
				vSpace = 0;
			}

			for (LinkedList<MiniNode2D> level : nodesOnShelf) {

				int x;
				int hSpace;

				if (level.size() > 1) {
					x = MARGINS_LR;
					hSpace = (getWidth() - (level.size() * NODE_WIDTH) - (2 * MARGINS_LR)) / (level.size() - 1);
				}
				else {
					x = getWidth() / 2;
					hSpace = 0;
				}

				for (MiniNode2D miniNode : level) {
					miniNode.setPosition(x, y);
					x += (NODE_WIDTH + hSpace);
				}

				// Going to next level height
				y -= (NODE_WIDTH + vSpace);
			}
		}
	}

	@Override
	protected void paintComponent(Graphics g) {
		super.paintComponent(g);

		traceTexturePath();

		Graphics2D g2 = (Graphics2D) g;

		g2.setPaint(new GradientPaint(getWidth() / 2, 0, gradientColorBegin, getWidth() / 2, getHeight(), gradientColorEnd));
		g2.fill(selectionZone);

		g2.setPaint(Color.BLACK);

		for (LinkedList<MiniNode2D> level : nodesOnShelf) {
			for (MiniNode2D miniNode : level) {
				g2.draw(new Ellipse2D.Float(miniNode.getPosition().x, miniNode.getPosition().y, NODE_WIDTH, NODE_WIDTH));
			}
		}

		paintChildren(g2);
	}

	/**
	 * Traces the shelf selection zone {@link GeneralPath}
	 */
	protected void traceTexturePath() {
		selectionZone = new GeneralPath();

		selectionZone.moveTo(TRAPEZOID_DELTA, 0);
		selectionZone.lineTo(getWidth() - TRAPEZOID_DELTA, 0);
		selectionZone.lineTo(getWidth(), getHeight());
		selectionZone.lineTo(0, getHeight());
		selectionZone.lineTo(TRAPEZOID_DELTA, 0);
	}

	/**
	 * Creates the {@link Thread} for the opening {@link Animation}
	 * 
	 * @return the created {@link Thread}
	 */
	public Animation getOpeningAnimation() {

		Thread anim = new Thread() {
			@Override
			public void run() {

				open();

				waitForAnimationTimer();

				pyramide2D.getAnimationMonitor().endReachedBy(this);
			}
		};

		anim.setName("Show Shelf");

		return new Animation(anim);
	}

	/**
	 * Creates the closing {@link Animation}
	 * 
	 * @return the created {@link Animation}
	 */
	public Animation getClosingAnimation() {

		Thread anim = new Thread() {
			@Override
			public void run() {

				close();

				waitForAnimationTimer();

				pyramide2D.getAnimationMonitor().endReachedBy(this);
			}
		};

		anim.setName("Close Shelf");

		return new Animation(anim);
	}

	/**
	 * Launches the opening
	 */
	public void open() {

		stopAnimationTimer();

		pyramide2D.add(this, 0);
		setBounds(0, pyramide2D.getHeight() - (int) (DEFAULT_HEIGHT / 1.5f), 0, 0);

		float dWidth = pyramide2D.getWidth() / (ANIMATION_LENGTH / ANIMATION_DELAY);
		float dHeight = DEFAULT_HEIGHT / (ANIMATION_LENGTH / ANIMATION_DELAY);

		// Width step correction
		if (dWidth < 0 && dWidth > -1) {
			dWidth = -1;
		}

		if (dWidth > 0 && dWidth < 1) {
			dWidth = 1;
		}

		// Height step correction
		if (dHeight < 0 && dHeight > -1) {
			dHeight = -1;
		}

		if (dHeight > 0 && dHeight < 1) {
			dHeight = 1;
		}

		final float deltaWidth = dWidth;
		final float deltaHeight = dHeight;

		ActionListener appearCallback = new ActionListener() {
			public void actionPerformed(ActionEvent evt) {
				maximize(deltaWidth, deltaHeight);
			}
		};

		animationTimer = new Timer(0, appearCallback);
		animationTimer.setDelay(ANIMATION_DELAY);
		animationTimer.start();
	}

	/**
	 * Launches the closing
	 */
	public void close() {

		stopAnimationTimer();

		clearNodesOnShelf();

		float dWidth = getWidth() / (ANIMATION_LENGTH / ANIMATION_DELAY);
		float dHeight = getHeight() / (ANIMATION_LENGTH / ANIMATION_DELAY);

		// Width step correction
		if (dWidth < 0 && dWidth > -1) {
			dWidth = -1;
		}

		if (dWidth > 0 && dWidth < 1) {
			dWidth = 1;
		}

		// Height step correction
		if (dHeight < 0 && dHeight > -1) {
			dHeight = -1;
		}

		if (dHeight > 0 && dHeight < 1) {
			dHeight = 1;
		}

		final int currentY = getY();
		final float deltaWidth = dWidth;
		final float deltaHeight = dHeight;

		ActionListener disappearCallback = new ActionListener() {
			public void actionPerformed(ActionEvent evt) {
				minimize(currentY, deltaWidth, deltaHeight);
			}
		};

		animationTimer = new Timer(0, disappearCallback);
		animationTimer.setDelay(ANIMATION_DELAY);
		animationTimer.start();
	}

	/**
	 * Callback function for <i>open()</i>
	 * 
	 * @param dWidth
	 *            interpolation step for the width
	 * @param dHeight
	 *            interpolation step for the height
	 */
	protected void maximize(float dWidth, float dHeight) {

		// Interpolation
		int width = (int) (getWidth() + dWidth);
		int height = (int) (getHeight() + dHeight);

		if (width >= pyramide2D.getWidth() || height >= DEFAULT_HEIGHT) {
			stopAnimationTimer();
			pyramide2D.repaint();
		}

		setBounds((pyramide2D.getWidth() / 2) - (width / 2), pyramide2D.getHeight() - Pyramide2D.MARGINS - (int) (height / 1.5f), width, height);
	}

	/**
	 * Callback function for <i>close()</i>
	 * 
	 * @param currentY
	 *            the shelf y coordinate on first call
	 * @param dWidth
	 *            interpolation step for the width
	 * @param dHeight
	 *            interpolation step for the height
	 */
	protected void minimize(int currentY, float dWidth, float dHeight) {

		// Interpolation
		int width = (int) (getWidth() - dWidth);
		int height = (int) (getHeight() - dHeight);

		if (width <= 0 || height <= 0) {
			stopAnimationTimer();
			pyramide2D.remove(this);
			pyramide2D.repaint();
		}

		setBounds((pyramide2D.getWidth() / 2) - (width / 2), currentY - (int) (height / 1.5f), width, height);
	}

	/**
	 * Close the shelf whithout animation
	 */
	public void closeNow() {
		if (isShowing()) {
			pyramide2D.remove(this);
			pyramide2D.repaint();
		}
	}

	/**
	 * Stops the <i>animationTimer</i>
	 */
	public synchronized void stopAnimationTimer() {
		if (animationTimer != null) {
			animationTimer.stop();
		}

		notify();
	}

	/**
	 * Waits for the <i>animationTimer</i> to stop
	 */
	public synchronized void waitForAnimationTimer() {
		while (animationTimer != null && animationTimer.isRunning()) {
			try {
				wait();
			}
			catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}

	/**
	 * Releases the shelf resources
	 */
	public void dispose() {

		/*
		 * Clearing the lists
		 */
		for (LinkedList<MiniNode2D> level : nodesOnShelf) {
			level.clear();
		}

		nodesOnShelf.clear();
	}
}
